"""Configuration Template Applicator.

Handles application of configuration templates to devices with conflict resolution,
rollback preparation, and batch application support.
"""

import asyncio
import json
import logging
import time
import uuid
from datetime import datetime
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field
from enum import Enum
from contextlib import asynccontextmanager

from .template_manager import (
    ConfigTemplate, TemplateManager, TemplateCategory, TemplateStatus,
    ValidationRule
)
from .batch_manager import BatchConfigManager, DeviceInfo, DeviceConfigResult
from ..utils.cache import LRUCache

logger = logging.getLogger(__name__)


class ApplicationStatus(Enum):
    """Template application status."""
    PENDING = "pending"           # Application pending
    VALIDATING = "validating"     # Validating template and device
    PREPARING = "preparing"       # Preparing configuration
    APPLYING = "applying"         # Applying configuration
    VERIFYING = "verifying"       # Verifying application
    COMPLETED = "completed"       # Application completed
    FAILED = "failed"            # Application failed
    ROLLBACK = "rollback"        # Rolling back changes


class ConflictResolution(Enum):
    """Template conflict resolution strategies."""
    MERGE_TEMPLATE_PRIORITY = "merge_template_priority"    # Template values override device
    MERGE_DEVICE_PRIORITY = "merge_device_priority"       # Device values override template
    REPLACE_ALL = "replace_all"                          # Replace all device config
    FAIL_ON_CONFLICT = "fail_on_conflict"               # Fail if conflicts detected
    PROMPT_USER = "prompt_user"                         # Prompt user for resolution


@dataclass
class ApplicationContext:
    """Template application context."""
    application_id: str                    # Unique application ID
    template: ConfigTemplate               # Template to apply
    target_devices: List[DeviceInfo]       # Target devices
    override_values: Dict[str, Any]        # Override values
    rollback_data: Dict[str, Any]         # Rollback configuration
    
    # Application options
    conflict_resolution: ConflictResolution = ConflictResolution.MERGE_TEMPLATE_PRIORITY
    dry_run: bool = False                  # Dry run mode
    backup_config: bool = True             # Backup existing config
    verify_application: bool = True        # Verify after application
    rollback_on_failure: bool = True       # Auto-rollback on failure
    
    # Status tracking
    status: ApplicationStatus = ApplicationStatus.PENDING
    progress: float = 0.0                  # Progress percentage
    start_time: Optional[datetime] = None   # Application start time
    end_time: Optional[datetime] = None     # Application end time
    
    # Results
    successful_devices: List[str] = field(default_factory=list)
    failed_devices: List[str] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)


@dataclass
class DeviceApplicationResult:
    """Individual device application result."""
    device_id: str                     # Device ID
    device_name: str                   # Device name
    status: ApplicationStatus          # Application status
    applied_config: Optional[Dict[str, Any]] = None  # Applied configuration
    backup_config: Optional[Dict[str, Any]] = None   # Backup configuration
    error_message: Optional[str] = None              # Error message
    warnings: List[str] = field(default_factory=list) # Warning messages
    duration: float = 0.0              # Application duration
    verification_passed: bool = False  # Verification result


@dataclass
class ConflictInfo:
    """Configuration conflict information."""
    field_path: str                    # Conflicting field path
    template_value: Any                # Value from template
    device_value: Any                  # Value from device
    resolution: Optional[Any] = None   # Resolved value
    resolution_strategy: Optional[ConflictResolution] = None


class TemplateApplicator:
    """Configuration template applicator."""
    
    def __init__(self, template_manager: TemplateManager, 
                 batch_manager: BatchConfigManager):
        """Initialize template applicator.
        
        Args:
            template_manager: Template manager instance
            batch_manager: Batch configuration manager
        """
        self.template_manager = template_manager
        self.batch_manager = batch_manager
        
        # Active applications
        self.active_applications: Dict[str, ApplicationContext] = {}
        
        # Results cache
        self.results_cache = LRUCache(capacity=100, ttl=3600)  # 1 hour TTL
        
        # Configuration
        self.max_concurrent_applications = 10
        self.application_timeout = 300  # 5 minutes
        self.verification_timeout = 60  # 1 minute
        self.backup_retention_days = 7
        
        # Concurrency control
        self.application_semaphore = asyncio.Semaphore(self.max_concurrent_applications)
        
        logger.info("TemplateApplicator initialized")
    
    async def apply_template_single(self, template_id: str, device_info: DeviceInfo,
                                  override_values: Optional[Dict[str, Any]] = None,
                                  options: Optional[Dict[str, Any]] = None) -> DeviceApplicationResult:
        """Apply template to a single device.
        
        Args:
            template_id: Template ID to apply
            device_info: Target device information
            override_values: Values to override in template
            options: Application options
            
        Returns:
            Application result
        """
        try:
            # Load template
            template = await self.template_manager.get_template(template_id)
            if not template:
                return DeviceApplicationResult(
                    device_id=device_info.device_id,
                    device_name=device_info.name,
                    status=ApplicationStatus.FAILED,
                    error_message=f"Template {template_id} not found"
                )
            
            # Create application context
            context = ApplicationContext(
                application_id=str(uuid.uuid4()),
                template=template,
                target_devices=[device_info],
                override_values=override_values or {},
                rollback_data={}
            )
            
            # Apply options
            if options:
                self._apply_application_options(context, options)
            
            # Apply to single device
            result = await self._apply_to_device(context, device_info)
            
            # Cache result
            self.results_cache.put(
                f"single_{template_id}_{device_info.device_id}", 
                result
            )
            
            return result
            
        except Exception as e:
            logger.error(f"Single template application error: {e}")
            return DeviceApplicationResult(
                device_id=device_info.device_id,
                device_name=device_info.name,
                status=ApplicationStatus.FAILED,
                error_message=str(e)
            )
    
    async def apply_template_batch(self, template_id: str, 
                                 target_devices: List[DeviceInfo],
                                 override_values: Optional[Dict[str, Any]] = None,
                                 options: Optional[Dict[str, Any]] = None) -> str:
        """Apply template to multiple devices.
        
        Args:
            template_id: Template ID to apply
            target_devices: Target devices
            override_values: Values to override in template
            options: Application options
            
        Returns:
            Application context ID for tracking
        """
        try:
            # Load template
            template = await self.template_manager.get_template(template_id)
            if not template:
                raise ValueError(f"Template {template_id} not found")
            
            # Create application context
            context = ApplicationContext(
                application_id=str(uuid.uuid4()),
                template=template,
                target_devices=target_devices,
                override_values=override_values or {},
                rollback_data={}
            )
            
            # Apply options
            if options:
                self._apply_application_options(context, options)
            
            # Store active application
            self.active_applications[context.application_id] = context
            
            # Start batch application
            asyncio.create_task(self._execute_batch_application(context))
            
            logger.info(f"Started batch template application: {context.application_id}")
            return context.application_id
            
        except Exception as e:
            logger.error(f"Batch template application error: {e}")
            raise
    
    async def get_application_status(self, application_id: str) -> Optional[ApplicationContext]:
        """Get application status.
        
        Args:
            application_id: Application context ID
            
        Returns:
            Application context or None if not found
        """
        return self.active_applications.get(application_id)
    
    async def cancel_application(self, application_id: str, 
                               reason: str = "User cancelled") -> bool:
        """Cancel active application.
        
        Args:
            application_id: Application context ID
            reason: Cancellation reason
            
        Returns:
            True if cancelled successfully
        """
        try:
            context = self.active_applications.get(application_id)
            if not context:
                return False
            
            context.status = ApplicationStatus.FAILED
            context.errors.append(f"Cancelled: {reason}")
            context.end_time = datetime.now()
            
            # Cleanup
            del self.active_applications[application_id]
            
            logger.info(f"Cancelled application: {application_id}")
            return True
            
        except Exception as e:
            logger.error(f"Cancel application error: {e}")
            return False
    
    async def rollback_application(self, application_id: str) -> bool:
        """Rollback a completed application.
        
        Args:
            application_id: Application context ID
            
        Returns:
            True if rollback successful
        """
        try:
            # Get application from cache or storage
            result_key = f"batch_{application_id}"
            cached_result = self.results_cache.get(result_key)
            
            if not cached_result:
                logger.warning(f"Application {application_id} not found for rollback")
                return False
            
            context = cached_result
            if not context.rollback_data:
                logger.warning(f"No rollback data for application {application_id}")
                return False
            
            # Create rollback context
            rollback_context = ApplicationContext(
                application_id=f"rollback_{application_id}",
                template=context.template,
                target_devices=context.target_devices,
                override_values=context.rollback_data,
                rollback_data={}
            )
            rollback_context.status = ApplicationStatus.ROLLBACK
            
            # Execute rollback
            success_count = 0
            for device in context.target_devices:
                if device.device_id in context.successful_devices:
                    result = await self._apply_to_device(rollback_context, device)
                    if result.status == ApplicationStatus.COMPLETED:
                        success_count += 1
            
            logger.info(f"Rollback completed for {success_count} devices")
            return success_count > 0
            
        except Exception as e:
            logger.error(f"Rollback error: {e}")
            return False
    
    async def validate_template_compatibility(self, template_id: str,
                                            device_info: DeviceInfo) -> Tuple[bool, List[str]]:
        """Validate template compatibility with device.
        
        Args:
            template_id: Template ID
            device_info: Device information
            
        Returns:
            Tuple of (is_compatible, compatibility_issues)
        """
        try:
            template = await self.template_manager.get_template(template_id)
            if not template:
                return False, [f"Template {template_id} not found"]
            
            issues = []
            
            # Check device type compatibility
            if (template.device_compatibility and 
                device_info.device_type not in template.device_compatibility):
                issues.append(f"Device type '{device_info.device_type}' not supported")
            
            # Check firmware version requirements
            if template.firmware_requirements:
                device_version = device_info.firmware_version
                required_version = template.firmware_requirements.get(
                    device_info.device_type
                )
                if required_version and device_version < required_version:
                    issues.append(
                        f"Firmware version {device_version} < required {required_version}"
                    )
            
            # Check template dependencies
            if template.dependencies:
                for dep_id in template.dependencies:
                    dep_template = await self.template_manager.get_template(dep_id)
                    if not dep_template:
                        issues.append(f"Dependency template {dep_id} not found")
            
            return len(issues) == 0, issues
            
        except Exception as e:
            logger.error(f"Compatibility validation error: {e}")
            return False, [str(e)]
    
    async def preview_template_application(self, template_id: str,
                                         device_info: DeviceInfo,
                                         override_values: Optional[Dict[str, Any]] = None
                                         ) -> Dict[str, Any]:
        """Preview template application without applying.
        
        Args:
            template_id: Template ID
            device_info: Device information
            override_values: Override values
            
        Returns:
            Preview information
        """
        try:
            template = await self.template_manager.get_template(template_id)
            if not template:
                return {"error": f"Template {template_id} not found"}
            
            # Get current device configuration
            current_config = await self._get_device_configuration(device_info)
            
            # Prepare configuration with overrides
            template_config = self._prepare_template_configuration(
                template, override_values or {}
            )
            
            # Detect conflicts
            conflicts = self._detect_configuration_conflicts(
                current_config, template_config
            )
            
            # Calculate changes
            changes = self._calculate_configuration_changes(
                current_config, template_config
            )
            
            return {
                "template_id": template_id,
                "template_name": template.name,
                "device_id": device_info.device_id,
                "device_name": device_info.name,
                "conflicts": [
                    {
                        "field": c.field_path,
                        "current_value": c.device_value,
                        "template_value": c.template_value
                    } for c in conflicts
                ],
                "changes": changes,
                "estimated_duration": self._estimate_application_duration(changes),
                "compatibility_issues": await self.validate_template_compatibility(
                    template_id, device_info
                )
            }
            
        except Exception as e:
            logger.error(f"Preview error: {e}")
            return {"error": str(e)}
    
    # Private Methods
    
    def _apply_application_options(self, context: ApplicationContext, 
                                 options: Dict[str, Any]):
        """Apply application options to context."""
        if "conflict_resolution" in options:
            context.conflict_resolution = ConflictResolution(
                options["conflict_resolution"]
            )
        if "dry_run" in options:
            context.dry_run = options["dry_run"]
        if "backup_config" in options:
            context.backup_config = options["backup_config"]
        if "verify_application" in options:
            context.verify_application = options["verify_application"]
        if "rollback_on_failure" in options:
            context.rollback_on_failure = options["rollback_on_failure"]
    
    async def _execute_batch_application(self, context: ApplicationContext):
        """Execute batch template application."""
        try:
            context.status = ApplicationStatus.VALIDATING
            context.start_time = datetime.now()
            
            # Validate template and devices
            await self._validate_batch_application(context)
            
            context.status = ApplicationStatus.PREPARING
            context.progress = 10.0
            
            # Prepare configurations
            await self._prepare_batch_configurations(context)
            
            context.status = ApplicationStatus.APPLYING
            context.progress = 20.0
            
            # Apply to all devices concurrently
            tasks = []
            for device in context.target_devices:
                task = asyncio.create_task(
                    self._apply_to_device_with_semaphore(context, device)
                )
                tasks.append(task)
            
            # Wait for all applications to complete
            results = await asyncio.gather(*tasks, return_exceptions=True)
            
            # Process results
            for i, result in enumerate(results):
                device = context.target_devices[i]
                if isinstance(result, Exception):
                    context.failed_devices.append(device.device_id)
                    context.errors.append(f"{device.name}: {str(result)}")
                elif result.status == ApplicationStatus.COMPLETED:
                    context.successful_devices.append(device.device_id)
                else:
                    context.failed_devices.append(device.device_id)
                    if result.error_message:
                        context.errors.append(f"{device.name}: {result.error_message}")
            
            # Update final status
            if context.failed_devices:
                context.status = ApplicationStatus.FAILED if not context.successful_devices else ApplicationStatus.COMPLETED
            else:
                context.status = ApplicationStatus.COMPLETED
            
            context.progress = 100.0
            context.end_time = datetime.now()
            
            # Cache results
            self.results_cache.put(f"batch_{context.application_id}", context)
            
            # Cleanup
            if context.application_id in self.active_applications:
                del self.active_applications[context.application_id]
            
            logger.info(f"Batch application completed: {context.application_id}")
            
        except Exception as e:
            logger.error(f"Batch application error: {e}")
            context.status = ApplicationStatus.FAILED
            context.errors.append(str(e))
            context.end_time = datetime.now()
    
    async def _apply_to_device_with_semaphore(self, context: ApplicationContext,
                                            device: DeviceInfo) -> DeviceApplicationResult:
        """Apply template to device with semaphore control."""
        async with self.application_semaphore:
            return await self._apply_to_device(context, device)
    
    async def _apply_to_device(self, context: ApplicationContext,
                             device: DeviceInfo) -> DeviceApplicationResult:
        """Apply template to individual device."""
        start_time = time.time()
        result = DeviceApplicationResult(
            device_id=device.device_id,
            device_name=device.name,
            status=ApplicationStatus.PENDING
        )
        
        try:
            result.status = ApplicationStatus.VALIDATING
            
            # Validate device compatibility
            is_compatible, issues = await self.validate_template_compatibility(
                context.template.template_id, device
            )
            
            if not is_compatible:
                result.status = ApplicationStatus.FAILED
                result.error_message = f"Compatibility issues: {', '.join(issues)}"
                return result
            
            result.status = ApplicationStatus.PREPARING
            
            # Get current device configuration
            if context.backup_config:
                result.backup_config = await self._get_device_configuration(device)
            
            # Prepare template configuration
            template_config = self._prepare_template_configuration(
                context.template, context.override_values
            )
            
            # Resolve conflicts
            resolved_config = await self._resolve_configuration_conflicts(
                result.backup_config or {}, template_config, 
                context.conflict_resolution
            )
            
            result.applied_config = resolved_config
            
            if context.dry_run:
                result.status = ApplicationStatus.COMPLETED
                result.verification_passed = True
                return result
            
            result.status = ApplicationStatus.APPLYING
            
            # Apply configuration to device
            apply_success = await self._apply_configuration_to_device(
                device, resolved_config
            )
            
            if not apply_success:
                result.status = ApplicationStatus.FAILED
                result.error_message = "Configuration application failed"
                return result
            
            # Verify application if requested
            if context.verify_application:
                result.status = ApplicationStatus.VERIFYING
                
                verification_passed = await self._verify_device_configuration(
                    device, resolved_config
                )
                
                result.verification_passed = verification_passed
                
                if not verification_passed:
                    result.warnings.append("Configuration verification failed")
                    
                    if context.rollback_on_failure and result.backup_config:
                        await self._apply_configuration_to_device(
                            device, result.backup_config
                        )
                        result.status = ApplicationStatus.ROLLBACK
                        result.error_message = "Verification failed, rolled back"
                        return result
            
            result.status = ApplicationStatus.COMPLETED
            
        except Exception as e:
            logger.error(f"Device application error for {device.name}: {e}")
            result.status = ApplicationStatus.FAILED
            result.error_message = str(e)
            
            # Attempt rollback on error
            if (context.rollback_on_failure and result.backup_config and
                result.status != ApplicationStatus.VALIDATING):
                try:
                    await self._apply_configuration_to_device(
                        device, result.backup_config
                    )
                    result.status = ApplicationStatus.ROLLBACK
                except Exception as rollback_error:
                    result.warnings.append(f"Rollback failed: {rollback_error}")
        
        finally:
            result.duration = time.time() - start_time
        
        return result
    
    async def _validate_batch_application(self, context: ApplicationContext):
        """Validate batch application requirements."""
        # Check template status
        if context.template.status != TemplateStatus.ACTIVE:
            raise ValueError(f"Template {context.template.name} is not active")
        
        # Validate all devices
        incompatible_devices = []
        for device in context.target_devices:
            is_compatible, issues = await self.validate_template_compatibility(
                context.template.template_id, device
            )
            if not is_compatible:
                incompatible_devices.append(f"{device.name}: {', '.join(issues)}")
        
        if incompatible_devices:
            raise ValueError(f"Incompatible devices: {'; '.join(incompatible_devices)}")
    
    async def _prepare_batch_configurations(self, context: ApplicationContext):
        """Prepare configurations for all devices."""
        # Pre-process template configuration
        base_config = self._prepare_template_configuration(
            context.template, context.override_values
        )
        
        # Store in context for reuse
        context.rollback_data["base_config"] = base_config
    
    def _prepare_template_configuration(self, template: ConfigTemplate,
                                      override_values: Dict[str, Any]) -> Dict[str, Any]:
        """Prepare template configuration with overrides."""
        # Start with template defaults
        config = template.default_values.copy()
        
        # Apply override values
        config = self._deep_merge_dict(config, override_values)
        
        return config
    
    def _detect_configuration_conflicts(self, current_config: Dict[str, Any],
                                      template_config: Dict[str, Any]) -> List[ConflictInfo]:
        """Detect configuration conflicts."""
        conflicts = []
        
        def check_conflicts(current_dict, template_dict, path=""):
            for key, template_value in template_dict.items():
                current_path = f"{path}.{key}" if path else key
                
                if key in current_dict:
                    current_value = current_dict[key]
                    
                    if isinstance(template_value, dict) and isinstance(current_value, dict):
                        check_conflicts(current_value, template_value, current_path)
                    elif current_value != template_value:
                        conflicts.append(ConflictInfo(
                            field_path=current_path,
                            template_value=template_value,
                            device_value=current_value
                        ))
        
        check_conflicts(current_config, template_config)
        return conflicts
    
    async def _resolve_configuration_conflicts(self, current_config: Dict[str, Any],
                                             template_config: Dict[str, Any],
                                             strategy: ConflictResolution) -> Dict[str, Any]:
        """Resolve configuration conflicts."""
        if strategy == ConflictResolution.REPLACE_ALL:
            return template_config.copy()
        
        elif strategy == ConflictResolution.MERGE_TEMPLATE_PRIORITY:
            return self._deep_merge_dict(current_config, template_config)
        
        elif strategy == ConflictResolution.MERGE_DEVICE_PRIORITY:
            return self._deep_merge_dict(template_config, current_config)
        
        elif strategy == ConflictResolution.FAIL_ON_CONFLICT:
            conflicts = self._detect_configuration_conflicts(current_config, template_config)
            if conflicts:
                conflict_paths = [c.field_path for c in conflicts]
                raise ValueError(f"Configuration conflicts: {', '.join(conflict_paths)}")
            return template_config.copy()
        
        else:  # PROMPT_USER - not implemented in this context
            return self._deep_merge_dict(current_config, template_config)
    
    def _deep_merge_dict(self, base_dict: Dict[str, Any], 
                        update_dict: Dict[str, Any]) -> Dict[str, Any]:
        """Deep merge two dictionaries."""
        result = base_dict.copy()
        
        for key, value in update_dict.items():
            if (key in result and isinstance(result[key], dict) and 
                isinstance(value, dict)):
                result[key] = self._deep_merge_dict(result[key], value)
            else:
                result[key] = value
        
        return result
    
    def _calculate_configuration_changes(self, current_config: Dict[str, Any],
                                       new_config: Dict[str, Any]) -> Dict[str, Any]:
        """Calculate configuration changes."""
        changes = {
            "added": [],
            "modified": [],
            "removed": []
        }
        
        def compare_configs(current_dict, new_dict, path=""):
            # Check for additions and modifications
            for key, new_value in new_dict.items():
                current_path = f"{path}.{key}" if path else key
                
                if key not in current_dict:
                    changes["added"].append({
                        "path": current_path,
                        "value": new_value
                    })
                elif current_dict[key] != new_value:
                    if isinstance(new_value, dict) and isinstance(current_dict[key], dict):
                        compare_configs(current_dict[key], new_value, current_path)
                    else:
                        changes["modified"].append({
                            "path": current_path,
                            "old_value": current_dict[key],
                            "new_value": new_value
                        })
            
            # Check for removals
            for key in current_dict:
                if key not in new_dict:
                    current_path = f"{path}.{key}" if path else key
                    changes["removed"].append({
                        "path": current_path,
                        "value": current_dict[key]
                    })
        
        compare_configs(current_config, new_config)
        return changes
    
    def _estimate_application_duration(self, changes: Dict[str, Any]) -> float:
        """Estimate application duration based on changes."""
        # Base time per change type (seconds)
        base_times = {
            "added": 2.0,
            "modified": 1.5,
            "removed": 1.0
        }
        
        total_time = 0.0
        for change_type, change_list in changes.items():
            if change_type in base_times:
                total_time += len(change_list) * base_times[change_type]
        
        # Add base overhead
        return max(total_time + 5.0, 10.0)  # Minimum 10 seconds
    
    async def _get_device_configuration(self, device: DeviceInfo) -> Dict[str, Any]:
        """Get current device configuration."""
        try:
            # Use batch manager to get device configuration
            # This is a mock implementation - would integrate with actual device
            return {
                "wifi": {
                    "ssid": "current_network",
                    "security": "WPA2"
                },
                "system": {
                    "timezone": "UTC",
                    "ntp_server": "time.nist.gov"
                }
            }
            
        except Exception as e:
            logger.error(f"Error getting device configuration: {e}")
            return {}
    
    async def _apply_configuration_to_device(self, device: DeviceInfo,
                                           config: Dict[str, Any]) -> bool:
        """Apply configuration to device."""
        try:
            # Use batch manager for actual application
            # This is a mock implementation
            await asyncio.sleep(1.0)  # Simulate application time
            return True
            
        except Exception as e:
            logger.error(f"Error applying configuration to device: {e}")
            return False
    
    async def _verify_device_configuration(self, device: DeviceInfo,
                                         expected_config: Dict[str, Any]) -> bool:
        """Verify device configuration."""
        try:
            # Get current configuration
            current_config = await self._get_device_configuration(device)
            
            # Compare with expected configuration
            # Simplified verification - would be more comprehensive in practice
            return self._configs_match(current_config, expected_config)
            
        except Exception as e:
            logger.error(f"Error verifying device configuration: {e}")
            return False
    
    def _configs_match(self, config1: Dict[str, Any], config2: Dict[str, Any]) -> bool:
        """Check if two configurations match."""
        try:
            return json.dumps(config1, sort_keys=True) == json.dumps(config2, sort_keys=True)
        except:
            return False
    
    async def cleanup(self):
        """Clean up applicator resources."""
        # Cancel active applications
        for application_id in list(self.active_applications.keys()):
            await self.cancel_application(application_id, "System cleanup")
        
        # Clear cache
        self.results_cache.clear()
        
        logger.info("TemplateApplicator cleanup completed")


# Example usage and testing
if __name__ == "__main__":
    import asyncio
    from unittest.mock import MagicMock
    
    async def test_template_applicator():
        """Test template applicator."""
        # Create mock dependencies
        template_manager = MagicMock()
        batch_manager = MagicMock()
        
        # Create applicator
        applicator = TemplateApplicator(template_manager, batch_manager)
        
        # Mock template
        mock_template = ConfigTemplate(
            template_id="test_template_001",
            name="Test WiFi Template",
            description="Test template for WiFi configuration",
            category=TemplateCategory.NETWORK,
            tags=["wifi", "network"],
            version="1.0.0",
            author="system",
            created_at=datetime.now(),
            updated_at=datetime.now(),
            status=TemplateStatus.ACTIVE,
            config_schema={},
            default_values={
                "wifi": {
                    "ssid": "TestNetwork",
                    "password": "test123",
                    "security": "WPA2"
                }
            },
            validation_rules=[],
            device_compatibility=["printer"],
            firmware_requirements={},
            dependencies=[],
            usage_count=0,
            success_rate=0.0,
            parent_template_id=None,
            version_history=[],
            is_locked=False
        )
        
        template_manager.get_template.return_value = mock_template
        
        # Create mock device
        device = DeviceInfo(
            device_id="device_001",
            name="Test Printer",
            mac_address="00:11:22:33:44:55",
            signal_strength=-50,
            device_type="printer",
            firmware_version="1.0.0",
            last_seen=datetime.now()
        )
        
        try:
            # Test single device application
            print("Testing single device application...")
            result = await applicator.apply_template_single(
                "test_template_001", device, {"wifi": {"ssid": "CustomNetwork"}}
            )
            
            print(f"Application result: {result.status}")
            print(f"Duration: {result.duration:.2f}s")
            
            # Test compatibility validation
            print("\nTesting compatibility validation...")
            is_compatible, issues = await applicator.validate_template_compatibility(
                "test_template_001", device
            )
            print(f"Compatible: {is_compatible}")
            print(f"Issues: {issues}")
            
            # Test preview
            print("\nTesting application preview...")
            preview = await applicator.preview_template_application(
                "test_template_001", device
            )
            print(f"Preview data: {json.dumps(preview, indent=2, default=str)}")
            
            # Test batch application
            print("\nTesting batch application...")
            devices = [device]  # Would normally have multiple devices
            
            application_id = await applicator.apply_template_batch(
                "test_template_001", devices, {"wifi": {"ssid": "BatchNetwork"}}
            )
            
            print(f"Batch application started: {application_id}")
            
            # Monitor progress
            for i in range(10):
                await asyncio.sleep(1)
                status = await applicator.get_application_status(application_id)
                if status:
                    print(f"Progress: {status.progress:.1f}% - Status: {status.status}")
                    if status.status in [ApplicationStatus.COMPLETED, ApplicationStatus.FAILED]:
                        break
            
            print(f"Successful devices: {status.successful_devices if status else []}")
            print(f"Failed devices: {status.failed_devices if status else []}")
            
        finally:
            await applicator.cleanup()
    
    # Run test
    asyncio.run(test_template_applicator())